package com.lyb.hardware.disk;

import com.lyb.Utils;
import com.lyb.constant.DiskConstant;
import com.lyb.constant.SystemConstant;

import java.io.*;
import java.nio.ByteBuffer;

/**
 * 磁盘类
 *
 * @author Mr.Alpaca potato
 * @version 1.4.0
 */
public class AlpacaDisk {
    /**
     * 磁盘文件真实物理路径
     */
    private final String diskPath;
    /**
     * 文件分配表，长度为128的字节数组
     * FAT[index]：索引为index的磁块的内容关联的下一个磁块号
     * 特定磁块号有特殊含义，解释如下：{
     * -1：文件结束，该磁块无下一关联磁块
     * 0：磁块空闲，该磁块可写入内容
     * (2,128)：磁块0、1用于存储文件分配表，2用于存储根目录，剩余磁块用于存储内容
     * }
     */
    private final byte[] fat;
    /**
     * 根目录(64字节)
     */
    private final byte[] rootDirectory;


    public AlpacaDisk() {
        /* 初始化磁盘属性 */
        this.diskPath = SystemConstant.DISK_PATH;
        this.fat = new byte[DiskConstant.FAT_LENGTH];
        this.rootDirectory = new byte[DiskConstant.ROOT_DIRECTORY_LENGTH];

        /* 读取磁盘文件，若磁盘文件不存在则重建一个 */
        File disk = new File(diskPath);
        if (!disk.exists()) {
            Utils.outCommandResult("[系统]: 磁盘不存在，重建磁盘文件");
            createDisk();
            Utils.outCommandResult("[系统]: 磁盘创建成功，系统重启");
        }

        /* 加载文件分配表与根目录 */
        load();
    }

    /**
     * 创建新磁盘
     */
    private void createDisk() {
        try (BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(new FileOutputStream(this.diskPath))) {
            ByteBuffer buffer = ByteBuffer.allocate(DiskConstant.DISK_LENGTH);
            /* 文件分配表 */
            byte[] fat = createFat();
            /* 根目录 */
            byte[] rootDirectory = createRootDirectory();
            /* 放入缓冲中 */
            buffer.put(fat);
            buffer.put(rootDirectory);
            /* 写入文件 */
            bufferedOutputStream.write(buffer.array());
            bufferedOutputStream.flush();
        } catch (IOException e) {
            Utils.outCommandResult("[系统]: 新磁盘创建失败，系统无法启动");
            /* 终止进程 */
            System.exit(200);
        }
    }

    /**
     * 创建文件分配表并返回
     *
     * @return byte[] 文件分配表
     */
    private byte[] createFat() {
        /* 文件分配表前三项以及根目录关联磁块都为-1 */
        byte[] fat = new byte[DiskConstant.FAT_LENGTH];
        for (int i = 0; i < 11; i++) {
            fat[i] = -1;
        }
        return fat;
    }

    /**
     * 创建根目录并返回
     *
     * @return byte[] 根目录基础目录项
     */
    private byte[] createRootDirectory() {
        ByteBuffer buffer = ByteBuffer.allocate(DiskConstant.ROOT_DIRECTORY_LENGTH);
        buffer.put(new DirectoryEntry("bin", " ", "w", 0, (byte) 3).getDirectoryEntry());
        buffer.put(new DirectoryEntry("hom", " ", "w", 0, (byte) 4).getDirectoryEntry());
        buffer.put(new DirectoryEntry("rot", " ", "w", 0, (byte) 5).getDirectoryEntry());
        buffer.put(new DirectoryEntry("lib", " ", "w", 0, (byte) 6).getDirectoryEntry());
        buffer.put(new DirectoryEntry("etc", " ", "w", 0, (byte) 7).getDirectoryEntry());
        buffer.put(new DirectoryEntry("dev", " ", "w", 0, (byte) 8).getDirectoryEntry());
        buffer.put(new DirectoryEntry("pro", " ", "w", 0, (byte) 9).getDirectoryEntry());
        buffer.put(new DirectoryEntry("usr", " ", "w", 0, (byte) 10).getDirectoryEntry());
        return buffer.array();
    }

    /**
     * 从磁盘中加载文件分配表以及根目录
     * 赋予对象属性
     */
    private void load() {
        /* 创建缓冲流读取磁盘文件 */
        try (BufferedInputStream buffer = new BufferedInputStream(new FileInputStream(diskPath))) {
            /*
                从磁盘中加载文件分配表(前128字节)
                若成功读取字节数不匹配则提示报错信息
            */
            if (buffer.read(this.fat) != DiskConstant.FAT_LENGTH) {
                Utils.outCommandResult("[系统|磁盘]: 文件分配表读取失败，请检查磁盘文件是否损坏");
            }

            /*
                从磁盘中加载根目录(64字节)
                若成功读取字节数不匹配则提示报错信息
             */
            if (buffer.read(this.rootDirectory) != DiskConstant.ROOT_DIRECTORY_LENGTH) {
                Utils.outCommandResult("[系统|磁盘]: 根目录读取失败，请检查磁盘文件是否损坏");
            }
        } catch (IOException e) {
            Utils.outCommandResult("[系统|磁盘]: 无法读取磁盘文件，退出系统");
            System.exit(200);
        }
    }


    /* ********** 读 ********** */

    /**
     * 读取指定磁块内容并以字节数组形式返回
     *
     * @param diskNum 指定磁盘号
     * @return 字节数组(64字节)，储存一块磁块字节内容
     * @throws IOException 异常，表示磁盘读取失败
     */
    public byte[] readBlock(byte diskNum) throws IOException {
        /* 定义字节数组存储磁块内容(64字节) */
        byte[] content = new byte[DiskConstant.DISKBLOCK_LENGTH];
        /* 以只读模式打开文件 */
        RandomAccessFile randomAccessFile = new RandomAccessFile(diskPath, "r");
        /* 文件指针定位到指定磁块位置 */
        randomAccessFile.seek(diskNum * DiskConstant.DISKBLOCK_LENGTH);
        /* 读取信息 */
        randomAccessFile.read(content);
        randomAccessFile.close();
        return content;
    }

    /**
     * 读取指定起始磁块及其关联磁块内容并以字节数组形式返回
     *
     * @param startDiskNum 起始磁盘号
     * @return 字节数组，储存多块磁块字节内容
     * @throws IOException 异常，表示磁盘读取失败
     */
    public byte[] readBlocks(byte startDiskNum) throws IOException {
        /* 定义缓冲存取所有内容 */
        ByteBuffer buffer = ByteBuffer.allocate(DiskConstant.DISK_LENGTH);
        /* 定义字节数组存储一块磁块内容(64字节) */
        byte[] subContent = new byte[DiskConstant.DISKBLOCK_LENGTH];
        /* 以只读模式打开文件 */
        RandomAccessFile randomAccessFile = new RandomAccessFile(diskPath, "r");
        byte diskNum = startDiskNum;
        do {
            /* 文件指针定位到指定磁块位置 */
            randomAccessFile.seek(diskNum * DiskConstant.DISKBLOCK_LENGTH);
            /* 读取信息 */
            randomAccessFile.read(subContent);
            buffer.put(subContent);
            /* 指向下一关联磁块号 */
            diskNum = getNextBlockNum(diskNum);
        } while (diskNum != -1);
        randomAccessFile.close();

        /* 缓冲指针指向真正存入内容的末尾 */
        buffer.flip();
        byte[] allContent = new byte[buffer.limit()];
        /* 读取真正存入的内容 */
        buffer.get(allContent);
        return allContent;
    }

    /**
     * 返回空闲磁块号，遍历文件分配表，将查找到的内容为0的第一个索引号返回
     * 查找不到返回 -1
     *
     * @return byte类型，表示空闲磁块号
     */
    public byte getFreeBlockNum() {
        for (byte i = 0; i < this.fat.length; i++) {
            if (this.fat[i] == 0) {
                return i;
            }
        }
        return -1;
    }

    /**
     * 获取当前磁盘剩余空闲空间
     *
     * @return int类型，表示剩余空闲空间字节数
     */
    public int getFreeLength() {
        int num = 0;
        for (byte index : this.fat) {
            if (index == 0) {
                num++;
            }
        }
        return num * DiskConstant.DISKBLOCK_LENGTH;
    }

    /**
     * 获取指定磁块号关联的所有磁块号(依据文件分配表)，并以数组形式返回
     *
     * @param startDiskNum 起始磁盘号
     * @return byte[] 表示搜索结果(包含指定磁盘号)
     */
    public byte[] getAssociatedBlockNum(byte startDiskNum) {
        ByteBuffer buffer = ByteBuffer.allocate(DiskConstant.BLOCKS_MAX_ASSOCIATION_NUM);
        do {
            buffer.put(startDiskNum);
            startDiskNum = getNextBlockNum(startDiskNum);
        } while (startDiskNum != -1);
        /* 缓冲指针指向真正存入内容的末尾 */
        buffer.flip();
        byte[] association = new byte[buffer.limit()];
        buffer.get(association);
        return association;
    }

    /**
     * 根据文件分配表中获取指定磁块关联的下一磁块，返回磁块号
     *
     * @param diskNum 指定磁块号
     * @return byte，表示关联的下一磁块号，-1表示无关联
     */
    public byte getNextBlockNum(byte diskNum) {
        return this.fat[diskNum];
    }


    /* ********** 写 ********** */

    /**
     * 将二进制文件内容写入连续磁块，返回起始磁块号
     *
     * @param content 文件内容(64字节的整数倍)
     * @return byte类型，表示起始磁盘号，若为-1表示写入失败
     * @throws IOException 异常，表示磁块写入失败
     */
    public byte writeBlocks(byte[] content) throws IOException {
        /* 获取一块空闲磁盘号作为起始磁盘号，若为-1表示当前磁盘已无空闲空间 */
        byte startDiskNum = getFreeBlockNum();
        if (startDiskNum == -1) {
            Utils.outCommandResult("[系统|磁盘]: 当前磁盘已无空闲空间，请先清理磁盘");
            return -1;
        }

        /* 计算存储该内容所需磁盘空间(磁块整数倍) */
        int num = content.length / DiskConstant.DISKBLOCK_LENGTH;
        int blockNum = (content.length % DiskConstant.DISKBLOCK_LENGTH == 0) ? num : num + 1;
        int length = blockNum * DiskConstant.DISKBLOCK_LENGTH;

        /* 若文件大于磁盘剩余空闲空间则返回 */
        if (length > getFreeLength()) {
            Utils.outCommandResult("[系统|磁盘]: 磁盘空间不足 ! ! !");
            return -1;
        }

        /* 定义缓冲存入数据 */
        ByteBuffer buffer = ByteBuffer.allocate(length);
        buffer.put(content);
        /* 将文件指针指向缓冲区头部 */
        buffer.rewind();
        /* 以读写模式打开文件 */
        RandomAccessFile randomAccessFile = new RandomAccessFile(diskPath, "rw");

        /* 定义一次存储一个磁块的量以及文件结束的磁块号 */
        byte[] subContent = new byte[DiskConstant.DISKBLOCK_LENGTH];
        byte diskNum = startDiskNum;
        byte endDiskNum = 0;
        for (int i = 0; i < blockNum; i++) {
            /* 文件指针定位到指定磁块位置 */
            randomAccessFile.seek(diskNum * DiskConstant.DISKBLOCK_LENGTH);
            buffer.get(subContent);
            /* 读取信息 */
            randomAccessFile.write(subContent);

            /*
                文件分配表中当前磁块先赋予一个临时值，然后获取下一空闲磁块号
                将空闲磁块号写入文件分配表中当前磁块号这一行，再将当前磁块号指向获取的空闲磁块号
            */
            endDiskNum = diskNum;
            this.fat[endDiskNum] = -1;
            diskNum = getFreeBlockNum();
            this.fat[endDiskNum] = diskNum;
        }
        /* 文件最后一块磁块指向的下一磁块号置为-1 */
        this.fat[endDiskNum] = -1;
        /* 关闭流并返回起始磁盘号 */
        randomAccessFile.close();
        return startDiskNum;
    }

    /**
     * 将二进制文件内容写入指定磁块，返回操作结果
     *
     * @param diskNum 指定磁盘索引
     * @param content 文件内容(小于等于64字节)
     * @return 布尔值，true表示写入成功
     * @throws IOException 异常，表示磁块写入失败
     */
    public boolean writeBlock(byte diskNum, byte[] content) throws IOException {
        /* 文件分配表和根目录区域不得修改 */
        if (diskNum <= 2) {
            Utils.outCommandResult("[系统|磁盘]: 磁盘根区域不得修改！");
            return false;
        }

        /* 定义缓冲存入数据 */
        ByteBuffer buffer = ByteBuffer.allocate(DiskConstant.DISKBLOCK_LENGTH);
        buffer.put(content);

        /* 以读写模式打开文件 */
        RandomAccessFile randomAccessFile = new RandomAccessFile(diskPath, "rw");
        /* 文件指针定位到指定磁块位置 */
        randomAccessFile.seek(diskNum * DiskConstant.DISKBLOCK_LENGTH);
        /* 写入 */
        randomAccessFile.write(buffer.array());
        /* 关闭流并返回结果 */
        randomAccessFile.close();
        return true;
    }

    /**
     * 将文件分配表写入磁盘文件，防止下次重启系统无法读取
     */
    public void writeFat() {
        try {
            /* 定义缓冲存入数据 */
            ByteBuffer buffer = ByteBuffer.allocate(DiskConstant.FAT_LENGTH);
            buffer.put(this.fat);
            /* 以读写模式打开文件 */
            RandomAccessFile randomAccessFile = new RandomAccessFile(diskPath, "rw");
            /* 文件指针定位到指定磁块位置 */
            randomAccessFile.seek(0);
            /* 写入 */
            randomAccessFile.write(buffer.array());
            /* 关闭流并返回起始磁盘号 */
            randomAccessFile.close();
        } catch (IOException e) {
            Utils.outCommandResult("[系统|磁盘]: 文件分配表更新失败，此次操作数据可能会在下次重启时无法读取");
        }
    }

    /**
     * 清空指定磁块内容，返回操作结果
     *
     * @param diskNum 指定磁块号
     * @return 布尔值，true表示清空成功
     */
    public boolean clearBlock(byte diskNum) {
        /* 创建一个无内容的字节数组赋给指定磁块 */
        try {
            boolean result = writeBlock(diskNum, new byte[DiskConstant.DISKBLOCK_LENGTH]);
            /* 若写入成功则将文件分配表中该文件状态置为空闲 */
            if (result) {
                this.fat[diskNum] = 0;
                return true;
            }
        } catch (IOException e) {
            Utils.outCommandResult("[系统|磁盘]: 磁盘操作出现错误");
        }
        return false;
    }

    /**
     * 根据文件分配表清空指定磁块及其关联磁块内容，返回操作结果
     *
     * @param startDiskNum 起始磁块号
     * @return 布尔值，true表示清空成功
     */
    public boolean clearBlocks(byte startDiskNum) {
        byte[] associatedBlockNum = getAssociatedBlockNum(startDiskNum);
        for (byte diskNum : associatedBlockNum) {
            if (!clearBlock(diskNum)) {
                Utils.outCommandResult("[系统|磁盘]: 清空关联磁块失败");
                return false;
            }
        }
        return true;
    }

    /**
     * 将指定的磁块占用(文件分配表对应索引置为-1)
     * 通常用于创建目录时占用磁块(磁块内并无内容)
     */
    public void occupyBlock(byte diskNum) {
        this.fat[diskNum] = -1;
    }

    /**
     * 将指定的磁块释放(文件分配表对应索引置为0)
     * 通常用于删除空目录时释放磁块(磁块内并无内容)
     */
    public void freeBlock(byte diskNum) {
        this.fat[diskNum] = 0;
    }


    /* ********** Getter & Setter ********** */

    public byte[] getRootDirectory() {
        return rootDirectory;
    }

    public byte[] getFat() {
        return fat;
    }
}
